In [1]:
import sys
import os
import contextlib
import pandas as pd 
import altair as alt
alt.data_transformers.enable("vegafusion")

sys.path.append(os.path.abspath(os.path.join('..', 'src')))
from gridSavers import (
    optimize_battery_schedule_df_flexible_end,
    run_weekly_optimization_with_daily_usage,
    simulate_all_nodes, 
    prepare_visualization, 
    plot_altair_separate
)

Grid Savers: Potential Benifits to Utilize V2G¶

By Rafe Chang, Manjari Asawa, Ruben Darancou

Repo Link: https://github.com/rafecchang/gridSavers

Using a Vehicle to Grid (V2G) system, utilities can benifit from lower residential demands during peak hours, which cuts the cost of purchasing electricity in the wholesale power market. In this report, we are interested in simulating the implementastion of such technology.

Introduction¶

  • placeholder introduce CAISO
  • placeholder introduce V2G

Assumptions¶

  • The capacity of hardware, software, and regulation is supportive for this technology to implement
  • The reduce in demand will not effect wholesale electricity pricing

Data¶

  • Local Marginal Price (LMP) of the Day Ahead Market (DAM) in CAISO on May 5th and May6th.
    • Retrieved throught CAISO's OASIS Website
      • PRICES/ Energy Prices/ Locational Marginal Prices
      • Date From 05/05/2025 to 05/06/2025; Market: DAM; Group: ALL_APNODES
    • Loaded as allNodes in this notebook
  • LMP of DAM at POD_ADLIN_1_UNITS-APND
    • Retrieved throught CAISO's OASIS Website
      • PRICES/ Energy Prices/ Locational Marginal Prices
      • Date From 04/07/2025 to 05/06/2025; Market: DAM; Group: POD_ADLIN_1_UNITS-APND
    • Loaded as monthly in this notebook
  • On average, American drivers drive 30.1 miles everyday.
  • For the charger side: We are using the Tesla Wall Connector that has a 11.5 kW output, which is equal to 44 miles. We assume the power input is the same rate.
  • For the EV/ battery side: Below is the Useable Battery Capacity of Full Electric Vehicles (Tesla)
Model Useable Battery Capacity (kWh)
S Dual Motor 95
S Plaid 95
X Dual Motor 95
X Plaid 95
Y Long Range AWD 75
Y Long Range AWD Launch Series 75
Y Long Range RWD 75
3 Performance 75
3 Long Range Dual Motor 75
3 Long Range RWD 75
Y RWD 60
3 57.5

Preprocessing¶

allNodes and monthly is preprocess with preprocess.ipynb.

Analysis¶

EDA¶

allNodes has 116,112 rows, each role contains the electricity price per MW in the given hour at the given node.

In [2]:
allNodes = pd.read_csv('../data/preprocessed/0506pre.csv')
allNodes['INTERVALSTARTTIME'] = pd.to_datetime(allNodes['INTERVALSTARTTIME'])
allNodes.head() 
Out[2]:
OPR_DT NODE MW INTERVALSTARTTIME INTERVALENDTIME Hour_Label
0 2025-05-05 AFPR_1_TOT_GEN-APND 34.41489 2025-05-05 00:00:00 2025-05-05 01:00:00 00:00-01:00
1 2025-05-05 AFPR_1_TOT_GEN-APND 34.37762 2025-05-05 01:00:00 2025-05-05 02:00:00 01:00-02:00
2 2025-05-05 AFPR_1_TOT_GEN-APND 34.11542 2025-05-05 02:00:00 2025-05-05 03:00:00 02:00-03:00
3 2025-05-05 AFPR_1_TOT_GEN-APND 33.97113 2025-05-05 03:00:00 2025-05-05 04:00:00 03:00-04:00
4 2025-05-05 AFPR_1_TOT_GEN-APND 34.23300 2025-05-05 04:00:00 2025-05-05 05:00:00 04:00-05:00
In [3]:
chart_allNode = alt.Chart(allNodes).mark_line(interpolate='step-after').encode(
    x=alt.X('INTERVALSTARTTIME:T', title='Time Interval'),
    y=alt.Y('MW:Q', title='Price ($/MWh)'),
    color='NODE:N',
    tooltip=['NODE', 'Hour_Label', 'MW']
).properties(
    title='Day-Ahead LMP ($/MWh) Over Time by Node',
    width=800,
    height=400
).interactive()

chart_allNode
Out[3]:

From the plot, we found high variability across different nodes. We want to find the ones with higher profitability to illustrate the benifit of V2G implementation.

V2G Modeling¶

Case Specifications:

  • The resident has an EV with a battery of 95 kWh
  • The resident has a charger of 11.5 kWh/h output
  • The resident uses the EV from 8:00 - 18:00 and is participated in V2G program controled by the utility from 18:00 - 8:00
  • The resident travels 30.1 miles a day

Method:

Used dynamic programming to compute the optimal schedule of charging and discharging a battery over a series of discrete time intervals (e.g., hours in a day) to maximize profit from electricity price arbitrage. Accounted for battery constraints (capacity, minimum state of charge, max charge/discharge rates.)

Applied the algorithm across all nodes in allNodes.

In [4]:
with contextlib.redirect_stdout(open(os.devnull, 'w')):
    node_profit_df = simulate_all_nodes(allNodes)
    
chart = alt.Chart(node_profit_df).mark_bar(color = '#3A7DC1').encode(
    x=alt.X('Total Profit ($):Q', title='Total Profit ($)'),
    y=alt.Y('count()', title='Count'),
    tooltip=[
        alt.Tooltip('Total Profit ($):Q', title='Total Profit ($)', format='.2f'),
        alt.Tooltip('count()', title='Node Count')
    ]
).properties(
    width=600,
    height=400,
    title='Count of Nodes by Arbitrage Profit'
)
chart
Out[4]:
In [5]:
negative_count = node_profit_df[node_profit_df['Total Profit ($)'] < 0].shape[0]
zero_count = node_profit_df[node_profit_df['Total Profit ($)'] == 0].shape[0]
positive_count = node_profit_df[node_profit_df['Total Profit ($)'] > 0].shape[0]

total = len(node_profit_df)

negative_pct = negative_count / total * 100
zero_pct = zero_count / total * 100
positive_pct = positive_count / total * 100

print("Total Profit accounts for the electricity used to charge EVs; on a sigle night simulation- ")
print(f"V2G system at {negative_pct:.2f}% of the nodes did not make enough to make up for EV charging")
print(f"V2G system at {zero_pct:.2f}% of the nodes made enough to make EV charging free")
print(f"V2G system at {positive_pct:.2f}% of the nodes made more than what it takes to charge their EVs")
Total Profit accounts for the electricity used to charge EVs; on a sigle night simulation- 
V2G system at 83.90% of the nodes did not make enough to make up for EV charging
V2G system at 13.41% of the nodes made enough to make EV charging free
V2G system at 2.69% of the nodes made more than what it takes to charge their EVs
In [6]:
node_profit_df.head(5)
Out[6]:
NODE Total Profit ($)
0 POD_ADLIN_1_UNITS-APND 4.982919
1 SLAP_PGNC-APND 4.934240
2 PGNC_1_PDRP19-APND 4.916442
3 PGNC_1_PDRP17-APND 4.916442
4 PGNC_1_PDRP01-APND 4.916442
  • Node POD_ADLIN_1_UNITS-APND appeared to have the most profitability with the one day of simulation. Let's retrieve more data from this location.
  • Below is the LMP of node POD_ADLIN_1_UNITS-APND from 2025-04-07 to 2025-05-06.
In [7]:
monthly = pd.read_csv('../data/preprocessed/monthlypre.csv')
monthly['INTERVALSTARTTIME'] = pd.to_datetime(monthly['INTERVALSTARTTIME'])
monthly.head()
Out[7]:
OPR_DT NODE MW INTERVALSTARTTIME INTERVALENDTIME Hour_Label
0 2025-04-07 POD_ADLIN_1_UNITS-APND 4.79520 2025-04-07 00:00:00 2025-04-07 01:00:00 00:00-01:00
1 2025-04-07 POD_ADLIN_1_UNITS-APND -6.72152 2025-04-07 01:00:00 2025-04-07 02:00:00 01:00-02:00
2 2025-04-07 POD_ADLIN_1_UNITS-APND -4.41947 2025-04-07 02:00:00 2025-04-07 03:00:00 02:00-03:00
3 2025-04-07 POD_ADLIN_1_UNITS-APND -7.62245 2025-04-07 03:00:00 2025-04-07 04:00:00 03:00-04:00
4 2025-04-07 POD_ADLIN_1_UNITS-APND -7.82635 2025-04-07 04:00:00 2025-04-07 05:00:00 04:00-05:00
In [8]:
chart_month = alt.Chart(monthly).mark_line(color = '#3A7DC1', interpolate='step-after').encode(
    x='INTERVALSTARTTIME:T',
    y='MW:Q',
    tooltip=['Hour_Label', 'MW']
).properties(
    title='Day-Ahead LMP ($/MWh) for Node- POD_ADLIN_1_UNITS-APND',
    width=800,
    height=400
).interactive()

chart_month
Out[8]:
In [9]:
with contextlib.redirect_stdout(open(os.devnull, 'w')):
    monthly_result = run_weekly_optimization_with_daily_usage(
        monthly,
        initial_soc=66.5,
        daily_miles=30.1,
        miles_per_kwh=3.83 
    )

print("By participating with V2G, the utility can profit", round(monthly_result["Profit ($)"].sum(), 2), "dollars with the EV owner benifit from charging for free during the window of April 7th to May 6th. ")
By participating with V2G, the utility can profit 28.21 dollars with the EV owner benifit from charging for free during the window of April 7th to May 6th. 
In [10]:
df_all = prepare_visualization(monthly_result, monthly)
chart = plot_altair_separate(df_all)
chart
Out[10]:

Discussion¶

By adding more battery storage and shaving the peak demand, potential grid construction cost for peak demand can also be avoided. Following are the findings and limitations of this project.

Findings¶

  • This simulation calculated how much utilities can benifit from arbitrage. In the case of node POD_ADLIN_1_UNITS-APND, a V2G system with Tesla Wall Connector paired with a Tesla Model S or Model X battery, can profit 27.99 dollars from arbitrage.
  • This simulation provide a budget reference for utilities to implement similar programs.

Limitations¶

  • This simulation did not consider potential costs to implement such technology.
  • This simulation also did not consider the Real-Time Market.
  • There is a lack of information on utilities' purchase at each node, and where the residential customers located at- therefore at this stage we did not calculate the potential profit for any specific utility.
  • Our submission for CAISO OASIS api has not been approved yet; therefore data retrieval is achieved manually at this stage of the project.